﻿/*
 * KLF Headless Browser
 * Written by Kris Oye <kristianoye@gmail.com>
 * Copyright (C) 2011-2012.  All rights reserved.
 */
namespace KLF.Lib.Browser.DOM.Events
{
    #region Using Statements

    using KLF.Lib.Browser.DOM.HTML;
    using System.Reflection;
    using System;

    #endregion

    /// <summary>
    /// The Event interface is used to provide contextual information about an
    /// event to the handler processing the event. An object which implements 
    /// the Event interface is generally passed as the first parameter to an
    /// event handler. More specific context information is passed to event
    /// handlers by deriving additional interfaces from Event which contain 
    /// information directly relating to the type of event they accompany. 
    /// These derived interfaces are also implemented by the object passed 
    /// to the event listener.
    /// </summary>
    public interface IEvent
    {
        bool bubbles { get; }

        bool cancelable { get; }

        IEventTarget currentTarget { get; }

        bool defaultPrevented { get; }

        int eventPhase { get; }

        void initEvent(string eventTypeArg, bool canBubbleArg, bool cancelableArg);

        bool isTrusted { get; }

        void preventDefault();

        void stopImmediatePropagation();

        void stopPropagation();

        IEventTarget target { get; }

        DOMTimeStamp timeStamp { get; }

        string type { get; }
    }

    /// <summary>
    /// The Event interface is used to provide contextual information about an
    /// event to the handler processing the event. An object which implements 
    /// the Event interface is generally passed as the first parameter to an
    /// event handler. More specific context information is passed to event
    /// handlers by deriving additional interfaces from Event which contain 
    /// information directly relating to the type of event they accompany. 
    /// These derived interfaces are also implemented by the object passed 
    /// to the event listener.
    /// </summary>
    public class Event : IEvent
    {
        #region Constants

        public const short CAPTURING_PHASE = 1;
        public const short AT_TARGET = 2;
        public const short BUBBLING_PHASE = 3;

        #endregion

        #region Constructors

        internal Event()
        {
            timeStamp = new DOMTimeStamp();
        }

        public Event(string _type)
            : this()
        {
            type = _type;
        }

        /// <summary>
        /// DOM4 Style Event Initializer/Constructor.
        /// </summary>
        /// <param name="_type"></param>
        /// <param name="eventInit"></param>
        public Event(string _type, object eventInit)
            : this(_type)
        {
            PropertyInfo[] props = eventInit
                .GetType()
                .GetProperties(
                    BindingFlags.Instance | 
                    BindingFlags.GetProperty | 
                    BindingFlags.Public | 
                    BindingFlags.NonPublic);

            //  Initialize the event object w/ the supplied dictionary object.
            foreach (PropertyInfo pi in props)
            {
                PropertyInfo piLocal = GetType().GetProperty(pi.Name,
                    BindingFlags.Instance |
                    BindingFlags.GetProperty |
                    BindingFlags.Public |
                    BindingFlags.NonPublic);

                if (piLocal != null)
                {
                    if (pi.PropertyType != piLocal.PropertyType)
                        throw new ArgumentException(
                            String.Format("Property '{0}' expects type {1}, not {2}",
                                piLocal.Name, piLocal.PropertyType.Name, pi.PropertyType.Name));

                    if (piLocal != null)
                        piLocal.SetValue(this,
                            pi.GetValue(eventInit, null), null);
                }
                else
                {
                    Console.WriteLine("Property '{0}' does not exist in {1}", pi.Name, GetType().Name);
                }
            }
        }

        #endregion

        #region DOM Level 2 Events Compliance

        /// <summary>
        /// Used to indicate whether or not an event is a bubbling event.
        /// If the event can bubble the value is true, else the value is false.
        /// </summary>
        public bool bubbles
        {
            get;
            internal set;
        }

        /// <summary>
        /// Used to indicate whether or not an event can have its default 
        /// action prevented. If the default action can be prevented the value
        /// is true, else the value is false.
        /// </summary>
        public bool cancelable
        {
            get;
            internal set;
        }

        public IEventTarget currentTarget
        {
            get;
            internal set;
        }

        public bool defaultPrevented
        {
            get;
            internal set;
        }

        /// <summary>
        /// Indicates the event has been dispatched.  initEvent may no longer
        /// have any effect.
        /// </summary>
        internal bool dispatched
        {
            get;
            set;
        }

        /// <summary>
        /// Used to indicate which phase of event flow is currently 
        /// being evaluated.
        /// </summary>
        public int eventPhase
        {
            get;
            internal set;
        }

        /// <summary>
        /// The initEvent method is used to initialize the value of an Event 
        /// created through the DocumentEvent interface. This method may only 
        /// be called before the Event has been dispatched via the 
        /// dispatchEvent method, though it may be called multiple times 
        /// during that phase if necessary. If called multiple times the 
        /// final invocation takes precedence. If called from a subclass of 
        /// Event interface only the values specified in the initEvent method
        /// are modified, all other attributes are left unchanged. 
        /// </summary>
        /// <param name="eventTypeArg"></param>
        /// <param name="canBubbleArg"></param>
        /// <param name="cancelableArg"></param>
        public void initEvent(string eventTypeArg, bool canBubbleArg, bool cancelableArg)
        {
            if (!dispatched)
            {
                type = eventTypeArg;
                bubbles = canBubbleArg;
                cancelable = cancelableArg;
                timeStamp = new DOMTimeStamp();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public bool isTrusted
        {
            get;
            internal set;
        }

        /// <summary>
        /// If an event is cancelable, the preventDefault method is used to 
        /// signify that the event is to be canceled, meaning any default 
        /// action normally taken by the implementation as a result of the 
        /// event will not occur. If, during any stage of event flow, the 
        /// preventDefault method is called the event is canceled. Any default 
        /// action associated with the event will not occur. Calling this 
        /// method for a non-cancelable event has no effect. Once 
        /// preventDefault has been called it will remain in effect throughout
        /// the remainder of the event's propagation. This method may be used
        /// during any stage of event flow. 
        /// </summary>
        public void preventDefault()
        {
            if (cancelable)
            {
                defaultPrevented = true;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public void stopImmediatePropagation()
        {
            stopped = true;
        }

        /// <summary>
        /// 
        /// </summary>
        public bool stopped
        {
            get;
            internal set;
        }

        /// <summary>
        /// The stopPropagation method is used prevent further propagation of 
        /// an event during event flow. If this method is called by any 
        /// EventListener the event will cease propagating through the tree. 
        /// The event will complete dispatch to all listeners on the current 
        /// EventTarget before event flow stops. This method may be used 
        /// during any stage of event flow. 
        /// </summary>
        public void stopPropagation()
        {
            stopped = true;
        }

        /// <summary>
        /// Used to indicate the EventTarget to which the event was 
        /// originally dispatched.
        /// </summary>
        public IEventTarget target
        {
            get;
            internal set;
        }

        /// <summary>
        /// Used to specify the time (in milliseconds relative to the epoch) 
        /// at which the event was created. Due to the fact that some 
        /// systems may not provide this information the value of timeStamp 
        /// may be not available for all events. When not available, a value 
        /// of 0 will be returned. Examples of epoch time are the time of the
        /// system start or 0:0:0 UTC 1st January 1970.
        /// </summary>
        public DOMTimeStamp timeStamp
        {
            get;
            internal set;
        }

        /// <summary>
        /// The name of the event (case-insensitive). 
        /// 
        /// The name must be an XML name.
        /// </summary>
        public virtual string type
        {
            get
            {
                return StringLookup.GetValue(_type);
            }
            set
            {
                switch (value.ToLowerInvariant())
                {
                    case HTMLConstants.HTML_EVENT_DOM_CONTENT_LOADED:
                    case HTMLConstants.HTML_EVENT_BLUR:
                    case HTMLConstants.HTML_EVENT_FOCUS:
                    case HTMLConstants.HTML_EVENT_LOAD:
                    case HTMLConstants.HTML_EVENT_RESET:
                    case HTMLConstants.HTML_EVENT_UNFOCUS:
                    case HTMLConstants.HTML_EVENT_UNLOAD:
                        bubbles = false;
                        cancelable = false;
                        break;

                    case HTMLConstants.HTML_EVENT_ABORT:
                    case HTMLConstants.HTML_EVENT_CHANGE:
                    case HTMLConstants.HTML_EVENT_ERROR:
                    case HTMLConstants.HTML_EVENT_RESIZE:
                    case HTMLConstants.HTML_EVENT_SCROLL:
                        bubbles = true;
                        cancelable = false;
                        break;

                    case HTMLConstants.HTML_EVENT_SUBMIT:
                        bubbles = true;
                        cancelable = true;
                        break;
                }
                _type = new StringLookup(value.ToLowerInvariant());
            }
        }
        internal StringLookup _type;

        #endregion

        #region DOM Level 3 Events Compliance
        #endregion

        public override string ToString()
        {
            return "[object Event]";
        }
    }
}
